package com.se.netty;

import com.model.HttpJsonContent;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;


@Slf4j
@ChannelHandler.Sharable
public class ServerHandler extends ChannelInboundHandlerAdapter {

    static Channel inChannel;
    static Channel outChannel;

    static ConcurrentHashMap<String, Channel> hostChannels = new ConcurrentHashMap<>();

    //SocketTransferHandler socketTransferHandler = new SocketTransferHandler();

    //@ChannelHandler.Sharable
    //node pipe  target.pipe(source)  source.pipe(target)
    private static class SocketTransferHandler extends ChannelInboundHandlerAdapter{

        Channel bind;
        boolean isClient;

        public SocketTransferHandler(Channel bindChannel, boolean isClient) {
            this.bind = bindChannel;
            this.isClient = isClient;
        }

        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            log.info("【源主机消息】: {}, {}, {}", ctx.channel(), bind, msg);

            bind.writeAndFlush(msg);
        }

        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            log.info("channelInactive : {}", ctx);
            ctx.close();
        }


    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        log.info("channelActive : {}", ctx.channel().id());
    }

    /**
     * @author hdu_huang
     * @date 2018/5/25
     * @funs 主要业务逻辑  减少不必要日志
     * ChannelHandler方法中调用sync()或await()方法，会有可能引起死锁  BlockingOperationException
     */

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        if (msg instanceof FullHttpRequest) {
            log.info("channelId : {}, msgType : {}", ctx.channel().id(), msg.getClass());
            handlerHttpRequest(ctx, (FullHttpRequest) msg);
        } else {
            log.info("channelId : {}, msg : {}", ctx.channel().id(), ((ByteBuf)msg).toString(Charset.defaultCharset())); //msg byteBuf 解码
            ctx.writeAndFlush(msg);
        }

    }

    //https://juejin.cn/post/7034715997445554184#heading-0
    private void handlerHttpRequest(ChannelHandlerContext ctx, FullHttpRequest request) {

        HttpHeaders headers = request.headers();
        String uri = request.uri(); // 1.只有 /name?id=1 (本机时候)    2.host+uri, 作代理时候
        String methodName = request.method().name();
        String protocolVersion = request.protocolVersion().text();

        //application/x-www-form-urlencoded;charset=UTF-8
        String contentType = headers.get("Content-Type");

        Map<String, List<String>> urlArgs = new QueryStringDecoder(request.uri()).parameters();
        Map<String, String> headerMap = new HashMap<>();
        headers.entries().forEach(stringStringEntry -> headerMap.put(stringStringEntry.getKey(), stringStringEntry.getValue()));

        HttpJsonContent httpJsonContent = new HttpJsonContent();
        httpJsonContent.setMethod(protocolVersion + " : " + methodName);
        httpJsonContent.setUrl(headers.get("Host") + uri);
        httpJsonContent.setUrlArgs(urlArgs);
        httpJsonContent.setHeader(headerMap);

        FullHttpRequest copy = request.copy();
        //copy.set

        // 构造FullHttpResponse对象，FullHttpResponse包含message body
        FullHttpResponse response = null;


        //! SSH端口转发也被称为SSH隧道 跳板机原理

        //http代理  1. server(http)充当中间人   2.CONNECT方法, proxy server和des建议TCP链接，实行数据盲转发(https)
        // 1.必须是完整的url  2.header中 Connection变Proxy-Connection
        //CONNECT www.google.com:443 HTTP/1.1 User-Agent: Java/11.0.1   Host: www.google.com Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
        //!Proxy-Connection: keep-alive  content-length: 0

        if (headers.contains("Proxy-Connection") && "CONNECT".equals(methodName)) {
            log.error("Proxy-Connection: {}", request);
            handlerHttpProxy(ctx, request, uri);
            return;
        }


        //http隧道模式返回值  HTTP/1.1 200 Connection Established\r\n\r\n
        String proxyResponse = protocolVersion + " 200 Connection Established\\r\\n\\r\\n";

        switch (methodName) {
            case "GET" : break;
            case "CONNECT" :
                //ctx.writeAndFlush(proxyResponse);
                log.error("CONNECT");
                return;
            default :  break;
        }


        if ("GET".equals(methodName)) {

            byte[] bytes = httpJsonContent.toJsonString().getBytes(StandardCharsets.UTF_8);
            response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer(bytes));
            response.headers().set("Content-Type", "application/json; charset=utf-8"); //text/html
            response.headers().set("Content-Length", Integer.toString(bytes.length));


        } else { //其他类型 POST/PUT/DELETE

            String params = request.content().toString(Charset.defaultCharset());

            if (contentType != null && contentType.contains("application/x-www-form-urlencoded")) {
                Map<String, List<String>> formData = new QueryStringDecoder("/a?" + params).parameters(); //获取body参数
                httpJsonContent.setFormData(formData);
            } else { //text json xml...
                httpJsonContent.setJsonData(params);
            }

            byte[] bytes = httpJsonContent.toJsonString().getBytes(StandardCharsets.UTF_8);
            response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer(bytes));
            response.headers().set("Content-Type", "application/json; charset=utf-8"); //text/html
            response.headers().set("Content-Length", Integer.toString(bytes.length));

        }
        ctx.writeAndFlush(response);
    }

    //https://juejin.cn/post/7034715997445554184
    //https://zhuanlan.zhihu.com/p/451074695  SOCKS
    //ProxyHandler  SocksPortUnificationServerHandler  [Socks4ServerEncoder和Socks5ServerEncoder]

    @SneakyThrows
    private void handlerHttpProxy(ChannelHandlerContext ctx, FullHttpRequest request, String url) {

        //! HTTP/1.1 200 Connection Established\r\n\r\n  直接复制会变"Established\\r\\n\\r\\n", 是错误的
        String proxyResponse = "HTTP/1.1 200 Connection Established\r\n\r\n";


        Channel channel = hostChannels.get(url);
        //if (channel != null) {
        //    removeAllHandlersAndResponse(ctx, proxyResponse, channel);
        //    return;
        //}

        if (outChannel != null && outChannel.isActive()) {
            removeAllHandlersAndResponse(ctx, proxyResponse, channel);
            return;
        }

        channel = new NioSocketChannel();
        NioEventLoopGroup group = (NioEventLoopGroup) ctx.executor().parent();
        group.register(channel);

        inChannel = ctx.channel();
        outChannel = channel;


        String[] urls = url.split(":");
        InetAddress byName = InetAddress.getByName(urls[0]);

        //BlockingOperationException  sync
        Channel finalChannel = channel;
        channel.connect(new InetSocketAddress(byName, Integer.parseInt(urls[1]))).addListener(future -> {

            if (future.isSuccess()) {

                hostChannels.putIfAbsent(url, finalChannel);
                //SocketTransferHandler transferHandler = new SocketTransferHandler();
                finalChannel.pipeline().addLast(new SocketTransferHandler(ctx.channel(), true));

                removeAllHandlersAndResponse(ctx, proxyResponse, finalChannel);
            }
        });

    }

    public void removeAllHandlersAndResponse(ChannelHandlerContext ctx, String proxyResponse, Channel bindChannel){

        ChannelPipeline pipeline = ctx.pipeline();
        List<String> names = pipeline.names();

        pipeline.names().forEach(s -> {
            if (!s.equals("DefaultChannelPipeline$TailContext#0")) {
                pipeline.remove(s);
            }
        });

        pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));

        ctx.channel().writeAndFlush(proxyResponse).addListener(future1 -> {
            if (future1.isSuccess()) {
                pipeline.remove(StringEncoder.class);
                pipeline.addLast(new SocketTransferHandler(bindChannel, false));
            }
        });
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        super.channelReadComplete(ctx);
        //ctx.flush();
    }

    /**
     * @author hdu_huang
     * @date 2018/6/4
     * @funs important (所有情况) 关闭 channel， remove maps
     */

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        //channel失效，从Map中移除
        super.channelInactive(ctx);
        log.info("channelInactive : {}", ctx.channel().id());
        ctx.close();
    }


    /**
     * @author hdu_huang
     * @date 2018/6/4
     * @funs 发生异常，关闭 channel
     */

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.info("exceptionCaught: {}, exception : {}, remote : {}", ctx.channel().id(), cause, ctx.channel().remoteAddress());
        ctx.close();

    }
}
